"""
Mengji system entry

date: 2025/3/19
author: SiHeng Tang
file: mengji.py
copyright(c) DFSA Eastern Software
此程序不能提供任何担保 WITHOUT WARRANTY OF ANY KIND
"""
import argparse
import asyncio
import json
import logging
import os
import os.path

from async_sche import MJScheduler
from mj_errors import MJRuntimeError
from mod_loader import load_mods_from, create_part_interface
from task_element import DataEndpoint, Wire
from web_api import MJWebApiServer

logger = logging.getLogger(__name__)


class MJArguments(object):
    """
    命令行参数解释器，命令行用法::

        mengji.py \
        --cfg /path/to/cfg.json \
        --web_api /tmp/mj_interface \
        --log_path /path/to/log_file \
        --log_level info
    """

    def __init__(self):
        parser = argparse.ArgumentParser(prog="mengji",
                                         description="Mengji framework execute entrance,"
                                                     "more information see: https://gitee.com/tang142857/ec130",
                                         epilog="Use this program with VERY CAUTION! IT WILL FLY!!",
                                         usage="python3 %(prog)s \\\n"
                                               "    --cfg /path/to/cfg.json \\\n"
                                               "    --web_api /path/to/web_socket \\\n"
                                               "    --log_path /path/to/log_file \\\n"
                                               "    --log_level level")
        parser.add_argument("--cfg", help="Mengji config file.")
        parser.add_argument("--web_api", help="Web api socket file location.")
        parser.add_argument("--log_path", help="Log file path.")
        parser.add_argument("--log_level", help="Log level, debug, info, warning, error.")
        self.paras = parser.parse_args()

        # 检查参数合理性
        # 确定参数都直接指定，配置文件存在，log_level 合理，日志位置有权限
        if (not self.paras.cfg) or (not self.paras.web_api) or (not self.paras.log_path) or (not self.paras.log_level):
            parser.print_help()
            exit(MJRuntimeError.INTERNAL_ERROR)

        if not os.path.isfile(self.paras.cfg):
            parser.print_help()
            exit(MJRuntimeError.INTERNAL_ERROR)

        if not (self.paras.log_level in ['debug', 'info', 'warning', 'error']):
            parser.print_help()
            exit(MJRuntimeError.INTERNAL_ERROR)

        logging_path = "/dev/null"
        if self.paras.log_path != 'nolog':
            logging_path = self.paras.log_path

        if not (os.access(os.path.dirname(logging_path) or '.', os.W_OK | os.X_OK) or
                (os.access(logging_path, os.W_OK))):
            parser.print_help()
            print("Please ensure we can write log there")
            exit(MJRuntimeError.INTERNAL_ERROR)

        # 处理日志文件
        # MJ 使用两个日志文件处理器，将默认处理器设为文件处理器，然后再启用一个控制台处理器
        # 传入的日志级别只能控制控制台日志，文件日志始终使用最详细的 DEBUG 级别
        logging.basicConfig(level=logging.DEBUG,
                            format='[%(levelname)s]%(asctime)s:%(module)s:%(message)s',
                            filename=logging_path,
                            filemode='a')

        console_handler = logging.StreamHandler()
        if self.paras.log_level == "debug":
            console_handler.setLevel(logging.DEBUG)
        elif self.paras.log_level == "info":
            console_handler.setLevel(logging.INFO)
        elif self.paras.log_level == "warning":
            console_handler.setLevel(logging.WARNING)
        elif self.paras.log_level == "error":
            console_handler.setLevel(logging.ERROR)
        console_handler.setFormatter(logging.Formatter('[%(levelname)s]%(asctime)s:%(module)s:%(message)s'))
        logging.getLogger('').addHandler(console_handler)

    def get_v(self, *args):
        """
        :param args: 使用变长参数查询，提供的条目 ``cfg_path`` ``web_api_path`` ``log_path``
        :return: 返回按参数列表顺序对应的值
        """
        return self.get_s(*args)

    def get_d(self, *args) -> dict:
        """
        字典风格成员存取器
        :param args: 请求成员列表
        :return: 字典键值对，使用传参列表作为键，不存在的成员使用 None 替代
        """
        return_d = {}

        for arg in args:
            if arg == "cfg_path":
                return_d.setdefault("cfg_path", self.paras.cfg)
            if arg == "web_api_path":
                return_d.setdefault("web_api_path", self.paras.web_api)
            if arg == "log_path":
                return_d.setdefault("log_path", self.paras.log_path)

        return return_d

    def get_s(self, *args) -> tuple:
        """
        列表风格成员存取器
        :param args: 请求成员列表
        :return: 按照请求成员顺序返回的列表，不存在的成员使用 None 替代
        """
        return tuple(self.get_d(*args).values())

    def get_a(self, k, default=None):
        """
        独立风格成员存取器
        :param k: 请求成员
        :param default: 默认返回值
        :return: 按照请求成员顺序返回的列表，不存在的成员使用 None 替代
        """
        return tuple(self.get_d(k).values())[0] if tuple(self.get_d(k).values())[0] else default


class MJConfigs(object):
    """
    配置文件工具解析，暂存和使用更简洁的方式查询配置。
    此方法与 MJ 历史开发和加载配置文件的方式有关，在新的配置加载方式中专用配置对象的作用被弱化，
    扩展应该自行维护一个默认值列表，然后使用传入的参数对默认值进行更新。
    """

    def __init__(self, cfg_path):
        try:
            with open(cfg_path, "r") as cfg_f:
                self.cfg = json.load(cfg_f)
        except FileNotFoundError as err:
            raise MJRuntimeError(MJRuntimeError.INTERNAL_ERROR, f"Bad config file {str(err)}.")

    @staticmethod
    def _is_num(s: str):
        try:
            return int(s)
        except ValueError:
            return s

    def get_a(self, ucl: str | None, default=None):
        """
        获取单项配置，直接返回相应映射后的 python 对象
        :param ucl: 通用定位器，在遇到数字时将数字转换为一个索引，其他情况下视为字符串，例如 ``wires:2:ep_a`` 等效 ``cfg[2]["ep_b"]``
        :param default: 访问键值不存在时的默认值
        :return:
        """
        if ucl is None:
            return self.cfg

        keys, parent = ucl.split(":"), self.cfg
        try:
            for k in keys:
                parent = parent[self._is_num(k)]
            return parent
        except KeyError:
            return default

    def get_v(self, ucl, default):
        """
        :param ucl: 通用定位器，例如 ``virtual_bus:console_ui:promote`` 等效 ``dict["virtual_bus"][""console_ui"]["promote"]``
        :param default: 默认值，查询失败时不抛出异常，而是使用 ``default`` 指定的值替代。
        """
        return self.get_a(ucl, default)


class Vehicle(object):
    """
    载具，管理所有运行成员，提供新成员注册和成员访问，实现 RIVER 模型的 wiring 操作。

    一个载具管理的运行成员包含了：

        1. 程序参数表 ``args``：程序运行时外部控制台传参。
        2. 配置文件 ``config``：自动按照 cfg 传参解析的配置文件，模块一般使用这个配置文件获取参数。
        3. 调度器 ``sche``：主要任务调度器，生成器都挂载在此调度器下。
        4. 走线 ``wire_hub``：在配置文件里实现的 wire 走线都在这里。
        5. 注册组件：最灵活的部分，注册组件保存通过注册函数提交的引用，并使用一个公共标签保存提供访问。

    组件注册和访问，组件注册在加载配置文件到管线连接间进行，可以访问的组件包含载具服务对象和所有动态模块：

        1. 注册使用 ``reg_part`` 成员函数，需要 label 和 对象引用，使用键值对元组。
        2. 访问使用 ``get_v`` 成员函数，除了基本参数表的 label 外直接使用相应 label 即可。
        3. 新风格的访问函数，``get_s`` ``get_d`` ``get_a`` 分别使用列表，字典和单值请求风格
    """
    SYSTEM_PARTS = ["args", "api", "config", "sche"]

    def __init__(self, args: MJArguments, api: MJWebApiServer):
        self._args = args
        self._api = api
        cfg_p, wapi_p, log_p = args.get_v("cfg_path", "web_api_path", "log_path")

        self._sche: MJScheduler = MJScheduler()
        self._config: MJConfigs = MJConfigs(cfg_p)
        self._dyn_parts = {}
        load_mods_from(self, "./vbus", "./processors")

        self._wire_endpoints()

        self._api.add_command("config", self.command_GET_CONFIG)

    def get_d(self, *args) -> dict:
        """
        字典风格成员存取器
        :param args: 请求成员列表
        :return: 字典键值对，使用传参列表作为键，不存在的成员使用 None 替代
        """
        return_d = {}
        for arg in args:
            if arg == "args":
                return_d.setdefault("args", self._args)
            elif arg == "api":
                return_d.setdefault("api", self._api)
            elif arg == "config":
                return_d.setdefault("config", self._config)
            elif arg == "sche":
                return_d.setdefault("sche", self._sche)
            else:
                return_d.setdefault(arg, self._dyn_parts.get(arg))

        return return_d

    def get_s(self, *args) -> tuple:
        """
        列表风格成员存取器
        :param args: 请求成员列表
        :return: 按照请求成员顺序返回的列表，不存在的成员使用 None 替代
        """
        return tuple(self.get_d(*args).values())

    def get_a(self, k, default=None):
        """
        独立风格成员存取器
        :param k: 请求成员
        :param default: 默认返回值
        :return: 按照请求成员顺序返回的列表，不存在的成员使用 None 替代
        """
        return tuple(self.get_d(k).values())[0] if tuple(self.get_d(k).values())[0] else default

    def reg_part(self, label: str, part_ref, is_sche=False):
        """
        注册组件位置参数风格接口
        :param is_sche: 是否直接注册为调度器生成器，默认否
        :param label: 组件标签
        :param part_ref: 组件引用
        """
        return self.reg_parts(is_sche, **{label: part_ref})

    def reg_parts(self, is_sche=False, **kwargs):
        """
        组件部件可变参数风格接口
        :param is_sche: 是否直接注册为调度器生成器，默认否，对此调用的所有部件生效
        :param kwargs: 使用变长参数键值对设置，和系统设置冲突或者使用已存在的键产生异常
        """
        for key, part in kwargs.items():
            if key in self.SYSTEM_PARTS:
                raise MJRuntimeError(MJRuntimeError.PROCESSOR_ERROR, f"{key} conflicts with system key.")
            if key in self._dyn_parts.keys():
                raise MJRuntimeError(MJRuntimeError.PROCESSOR_ERROR, f"{key} conflicts with exist key.")

            self._dyn_parts.setdefault(key, create_part_interface(part))
            # self._dyn_parts.setdefault(key, part)
            if is_sche:
                self._sche.reg_generator(part)

    async def execute(self):
        """
        整个载具事件跑在一个大循环里面::

            1. 处理 web API 上的指令
            2. 执行生成器
        """
        logger.info("starting main event loop.")
        while True:
            self._api.handle_requests()
            await self._sche.next_loop()

    def exit(self):
        self._sche.finalize()

        self._args = None
        self._api = None
        self._sche = None
        self._dyn_parts = None
        self._config = None

    def _wire_endpoints(self):
        # 自动接线先从配置文件提取 wires 配置，获得所有的端点标签信息
        # 使用成员访存接口获取成员和端点，使用 wire 类接线，并注册到生成器
        # 自动接线使用匿名方法创建载具成员，不会在动态模块注册
        all_wires = self._config.get_a("wires")
        for w_arg in all_wires:
            ep_a_mod, ep_a_label = w_arg["ep_a"].split(":")
            ep_b_mod, ep_b_label = w_arg["ep_b"].split(":")
            mod_a, mod_b = self.get_s(ep_a_mod, ep_b_mod)

            # 三步确认，模块存在，端点存在，端点是一个正确的对象
            if (not mod_a) or (not mod_b):
                logger.critical(f"target mod {ep_a_mod=} or {ep_b_mod=} not exist")
                continue
            if (not hasattr(mod_a, ep_a_label)) or (not hasattr(mod_b, ep_b_label)):
                logger.critical(f"target mod has no {ep_a_label=} or {ep_b_label=} ep")
                continue
            if type(getattr(mod_a, ep_a_label)) != DataEndpoint or type(getattr(mod_b, ep_b_label)) != DataEndpoint:
                logger.critical(f"invalid endpoint type in {ep_a_label=} or {ep_b_label=}")
                continue

            ep_a, ep_b = getattr(mod_a, ep_a_label), getattr(mod_b, ep_b_label)
            self._sche.reg_generator(Wire(ep_a=ep_a, ep_b=ep_b))
            logger.info(f"wiring target {ep_a_mod=}:{ep_a_label=} at {ep_a} {ep_b_mod=}:{ep_b_label=} at {ep_b}")

    def command_GET_CONFIG(self, rpc_params: list):
        """
        查询配置文件
        :param rpc_params: 不使用此值
        :return: 整个配置文件，以字典对象返回
        """
        return self._config.get_a(None)

    def command_WRITE_CONFIG(self, rpc_params: dict):
        """
        写入新配置文件，此方法会校验配置文件是否包含所有需要的元素
        :param rpc_params: 完整的配置文件对象
        :return: 设置成功返回 0
        """
        # TODO write config file
        return 0


async def main():
    """
    主函数处理逻辑：

        1. 首次运行初始化程序参数对象，使用 ``NORMAL_ENTRY_CODE`` 错误代码，空提示。
        2. 初始化载具对象，完成后调用 ``execute`` 成员启动载具任务循环
        3. 捕获载具异常，退出异常使用相同的错误代码退出程序
        4. 重新初始化程序参数，使用上一次的提出异常
    """
    args = MJArguments()
    api = MJWebApiServer(args.get_a("web_api_path", "/tmp/mj_interface"))
    veh = None

    api.start()
    logger.info("Mengji framework in gicg project. Starting...")

    try:
        veh = Vehicle(args, api)
        await veh.execute()

    except MJRuntimeError as err:
        if not err.is_the_err(MJRuntimeError.EXIT_ERROR):
            logger.error(f"system failed {str(err)}")
        exit(err.get_err_code())

    except KeyboardInterrupt:
        print("Keyboard exit...")
        exit(0)

    api.disconnect()
    veh.exit()
    # GC 这个事情没办法，做不到重启的时候绝对理想清理内存，完全清空重启还是需要退出再进入，所以没有再做这方面的挣扎 :)
    # 程序退出后会自动通过 systemd 重新启动，此默认行为适合重新启动载入新配置文件或完全重置任务的情况。
    # 对于以外的任务中退出重启，任务管理实现必须能检测这种意外并降级为自动驾驶仪返航或其他安全行为。


if __name__ == '__main__':
    asyncio.run(main())
